// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu, jhkim

#pragma once

#include <memory>
#include <vector>
#include <map>
#include <string>
#include <utility>
#include <unordered_map>

#include "coin/proto/coin_executor.pb.h"
#include "coin/proto/coin_order_enums.pb.h"
#include "coin/proto/coin_telemetry.pb.h"

#include "coin2/exchange/base/log/strategy_logger.h"
#include "coin2/exchange/base/symbology/product_encyclopedia.h"

#include "coin2/strategy/linear_model/logic.h"
#include "coin2/strategy/linear_model/util.h"
#include "coin2/strategy/order_executor/executor.h"

namespace coin2::strategy::linear_model {

using ::coin2::exchange::base::symbology::ProductEncyclopedia;
using coin::proto::OrderDirection;

// represents 1 symbol
class LmAggLogic2 : public LinearModelLogic {
 public:
  explicit LmAggLogic2(
      const MarketExchangeApi& mea,
      const IProduct& target_product,
      const Symbol* target_symbol,
      const LmProductConfig& lm_product_config,
      ProductEncyclopedia* product_cache)
      : LinearModelLogic(mea, target_product, target_symbol, product_cache),
        enable_telemetry_(lm_product_config.enable_telemetry()),
        mea_(mea),
        leverage_rate_(lm_product_config.leverage_rate()),
        lm_agg_config_(lm_product_config.agg_config()),
        threshold_bps_(
            lm_agg_config_.threshold_bps()
            + lm_agg_config_.threshold_bps_add()),
        cancel_edge_bps_(
            lm_agg_config_.cancel_edge_bps()
            + lm_agg_config_.cancel_edge_bps_add()),
        posting_edge_bps_add_(lm_agg_config_.posting_edge_bps_add()),
        post_cooltime_(lm_agg_config_.post_cooltime_sec() * 1e9),
        taker_cooltime_(lm_agg_config_.taker_cooltime_sec() * 1e9),
        fill_cooltime_(lm_agg_config_.fill_cooltime_sec() * 1e9),
        pred_interval_(lm_agg_config_.prediction_window_sec() * 1e9),
        clear_edge_manager_(
            lm_agg_config_.add_clear_lean_per_window_sec(),
            lm_agg_config_.add_clear_lean_bps()),
        filldepth_window_(
            lm_agg_config_.filldepth_window_sec(),
            lm_agg_config_.filldepth_window_pred_ms(),
            lm_agg_config_.filldepth_window_y_horiz_sec() * 1e9,
            lm_agg_config_.filldepth_window_signal_err_sec() * 1e9,
            lm_agg_config_.reversion_window_sec() * 1e9,
            lm_agg_config_.reversion_sw_sec() * 1e9,
            lm_agg_config_.tangent_sec() * 1e9,
            100),  // 100 ns sampling interval to sample bookp to measure filldepth mean & std
        rest_mode_(false),
        prev_submit_ts_(0),
        prev_executor_config_ts_(0),
        prev_ask_post_time_(0),
        prev_bid_post_time_(0),
        prev_sell_taker_time_(0),
        prev_buy_taker_time_(0),
        last_feed_timestamp_(0),
        sim_account_id_(lm_product_config.sim_account_id()),
        prev_bid_fill_ts_(0),
        prev_ask_fill_ts_(0),
        prev_bid_pfill_ts_(0),
        prev_ask_pfill_ts_(0),
        bid_fill_qty_(0),
        ask_fill_qty_(0),
        prev_bid_fill_price_(0),
        prev_ask_fill_price_(0),
        buy_edge_add_(0),
        sell_edge_add_(0),
        prev_buy_def_edge_reduced_ts_(0),
        prev_sell_def_edge_reduced_ts_(0),
        def_edge_ns_(lm_agg_config_.defensive_edge_sec() * Time::kNanosecondsPerSecond),
        new_bid_fill_(false),
        new_ask_fill_(false) {
    LOG(INFO) << "[LmAggLogic2] config json = " << lm_agg_config_.DebugString();
    LOG(INFO) << "TELEMETRY SWITCH: " << enable_telemetry_;

    // default is 0.65. used only when midp_model = true
    penalize_spread_ratio_ = lm_agg_config_.penalize_spread_ratio();
    if (lm_product_config.profile_name().find("_midret") != std::string::npos) {
      LOG(ERROR) << "USE MIDP MODEL";
      midp_model_ = true;
    } else {
      midp_model_ = false;
    }
    CHECK(midp_model_);
  }

  void InitImpl(
      ::coin2::app::Driver* driver,
      coin2::exchange::base::strategy_util::StrategyLogger* strat_logger) override {
    auto curr_time = GetCurrentTimestamp();
    filldepth_window_.SetStratLogger(strat_logger);

    std::string log_root = strat_logger->GetLogRoot();
    std::string machine = strat_logger->GetMachine();
    coin::proto::StrategyRequestProto strat_request = strat_logger->GetStrategyRequestProto();
    const size_t BUF_SIZE = 100 * 1024;

    coin2::exchange::base::strategy_util::StrategyLogReader reader(
        strat_request, log_root, machine,
        curr_time - 300 * Time::kNanosecondsPerSecond, curr_time, BUF_SIZE);

    coin::proto::StrategyStoredValue ssv_loaded[FilldepthWindow::stored_value_type::N_TYPES];

    coin::proto::StrategyLog strat_log_proto;
    std::string prod_name = lholder_.target_product.relative_norm();
    bool data_okay = true;

    if (lm_agg_config_.use_pre_reboot_values()) {
      while (reader.Read(&strat_log_proto)) {
        if (strat_log_proto.type() != coin::proto::StrategyLog::STRAT_STORED_VALUE) {
          continue;
        }

        if (strat_log_proto.strat_stored_value().product_name() != prod_name) {
          continue;
        }

        int value_type = strat_log_proto.strat_stored_value().value_type();
        if (value_type < FilldepthWindow::stored_value_type::N_TYPES) {
          ssv_loaded[value_type] = strat_log_proto.strat_stored_value();
          double stored_average = ssv_loaded[value_type].average();
          double stored_std = ssv_loaded[value_type].simplestd();
          if (stored_average * stored_average + stored_std * stored_std > 1e6) {
            // implied edge cannot be in sane range => skip loading
            data_okay = false;
            LOG(INFO) << "implied RMSE of pre-reboot data is too large:\n"
                << strat_log_proto.DebugString();
            break;
          }
        } else {
          data_okay = false;
          LOG(INFO) << "pre-reboot data has invalid values:\n"
              << strat_log_proto.DebugString();
          break;
        }
      }

      if (!data_okay) {
        for (int i = 0; i < FilldepthWindow::stored_value_type::N_TYPES; i++) {
          ssv_loaded[i].set_timestamp_begin(0L);
          ssv_loaded[i].set_timestamp_end(0L);
        }
      }

      LOG(INFO) << "tried loading pre-reboot values of buy_filldepths:\n"
          << ssv_loaded[0].DebugString();

      LOG(INFO) << "tried loading pre-reboot values of sell_filldepths:\n"
          << ssv_loaded[1].DebugString();

      LOG(INFO) << "tried loading pre-reboot values of signal_errors:\n"
          << ssv_loaded[2].DebugString();
    } else {
      for (int i = 0; i < FilldepthWindow::stored_value_type::N_TYPES; i++) {
        ssv_loaded[i].set_timestamp_begin(0L);
        ssv_loaded[i].set_timestamp_end(0L);
      }
    }

    filldepth_window_.SetPreRebootValues(curr_time, prod_name, ssv_loaded);

    for (auto& [name, oe] : driver->strategy()->order_executors(sim_account_id_)) {
      (void)name;
      if (oe->mea() == mea_) {
        lholder_.InitExecutor(oe.get(), strat_logger, enable_telemetry_, curr_time);
      }
    }
  }

  void OnFill(
      int64_t timestamp,
      coin::proto::TradeSide trade_side,
      double fill_price,
      double fill_qty) final {
    if (pos_mismatch_calculator_) {
      int sign = 0;
      switch (trade_side) {
        case coin::proto::TRADE_BUY_SIDE:
          sign = 1;
          break;
        case coin::proto::TRADE_SELL_SIDE:
          sign = -1;
          break;
        default:
          break;
      }
      // ignore timestamp. buggy.
      pos_mismatch_calculator_->UpdateFill(last_feed_timestamp_, sign, fill_qty);
    }
  }

  void UpdateMidp(
      StrategyReporter2* strat_reporter,
      const IProduct* target_product_given) final {
    strat_reporter->UpdateMidp(*target_product_given, GetMidPrice());
    lholder_.UpdateMidp(strat_reporter, target_product_given);
  }

  void onAccountInfo(const coin2::exchange::base::order::OrderUpdate& upd) final {
    if (upd.sim_account_id && sim_account_id_ != *upd.sim_account_id) return;
    if (lholder_.product_holder) lholder_.oe->onAccountInfo(*lholder_.product_holder, upd);
  }

  void onAccountOrder(const coin2::exchange::base::order::OrderUpdate& upd) final {
    if (upd.sim_account_id && sim_account_id_ != *upd.sim_account_id) return;
    if (lholder_.product_holder) lholder_.oe->onAccountOrder(*lholder_.product_holder, upd);

    if (!lholder_.product_holder) {
      LOG(INFO) << "Product holder not initialized yet";
      return;
    }

    double lot_size = lholder_.oe->GetConfig(*lholder_.product_holder).qty().max_lot_size();
    lot_size *= lm_agg_config_.maker_qty_ratio();
    auto& event = upd.product_order_info().event();
    int64_t id = event.proc_order_id();
    std::string tag = lholder_.oe->oq(*lholder_.product_holder)->
        order_manager().FindByProcOrderId(id)->order_spec().tag;
    if (event.type() == coin::proto::OrderEvent::ORDER_FILLED &&
        tag == "passive_post") {
      coin::proto::TradeSide trade_side = event.trade_side();
      if (trade_side == coin::proto::TRADE_BUY_SIDE) {
        new_bid_fill_ = true;
        bid_fill_qty_ += event.fill_qty();
        prev_bid_fill_price_ = event.fill_price();
        prev_bid_pfill_ts_ = event.event_time();
        if (bid_fill_qty_ >= lot_size) {
          prev_bid_fill_ts_ = event.event_time();
          bid_fill_qty_ = 0;
        }
      } else if (trade_side == coin::proto::TRADE_SELL_SIDE) {
        new_ask_fill_ = true;
        ask_fill_qty_ += event.fill_qty();
        prev_ask_fill_price_ = event.fill_price();
        prev_ask_pfill_ts_ = event.event_time();
        if (ask_fill_qty_ >= lot_size) {
          prev_ask_fill_ts_ = event.event_time();
          ask_fill_qty_ = 0;
        }
      }
    }
  }

  void onFeed(
      const Symbol* symbol,
      const FeedUpdate& upd,
      const ::fastfeature::FeedMessage& feed_msg,
      LinearModel* linear_model,
      int64_t* lm_feed_id,
      const int64_t* global_feed_id,
      LinearModelDelegateProto* delegate,
      bool simulation) override;

  void TryTrigger(
      const Symbol* symbol,
      const FeedUpdate& upd,
      const ::fastfeature::FeedMessage& feed_msg,
      LinearModel* linear_model,
      int64_t* lm_feed_id,
      const int64_t* global_feed_id,
      LinearModelDelegateProto* delegate,
      int64_t timestamp_override,
      std::optional<double>* midp_return);

 private:
  const bool enable_telemetry_;
  const MarketExchangeApi mea_;
  const double leverage_rate_;

  /* -------------------------------------------------------------------------- */
  /*                               strategy config                              */
  /* -------------------------------------------------------------------------- */

  LmAggLogicConfig lm_agg_config_;
  const double threshold_bps_;
  const double cancel_edge_bps_;
  const double posting_edge_bps_add_;
  const int64_t post_cooltime_;
  const int64_t taker_cooltime_;
  const int64_t fill_cooltime_;
  PredictionInterval pred_interval_;
  ClearEdgeManager clear_edge_manager_;
  FilldepthWindow filldepth_window_;
  bool rest_mode_;

  int64_t prev_submit_ts_;
  int64_t prev_executor_config_ts_;
  bool midp_model_;
  double penalize_spread_ratio_;
  std::optional<PositionMismatchCalculator> pos_mismatch_calculator_;
  int64_t prev_ask_post_time_;
  int64_t prev_bid_post_time_;
  int64_t prev_sell_taker_time_;
  int64_t prev_buy_taker_time_;
  int64_t last_feed_timestamp_;
  int64_t sim_account_id_;

  int64_t prev_bid_fill_ts_;
  int64_t prev_ask_fill_ts_;

  int64_t prev_bid_pfill_ts_;
  int64_t prev_ask_pfill_ts_;

  double bid_fill_qty_;
  double ask_fill_qty_;

  double prev_bid_fill_price_;
  double prev_ask_fill_price_;

  double buy_edge_add_;
  double sell_edge_add_;

  int64_t prev_buy_def_edge_reduced_ts_;
  int64_t prev_sell_def_edge_reduced_ts_;

  int64_t def_edge_ns_;

  bool new_bid_fill_;
  bool new_ask_fill_;
};

}  // namespace coin2::strategy::linear_model
